Skip to content

refactor: platform abstraction layer + fix macOS stdin hang#55

Merged
Sunrisepeak merged 6 commits into
mainfrom
refactor/platform-abstraction
May 20, 2026
Merged

refactor: platform abstraction layer + fix macOS stdin hang#55
Sunrisepeak merged 6 commits into
mainfrom
refactor/platform-abstraction

Conversation

@Sunrisepeak
Copy link
Copy Markdown
Member

Summary

  • Extracts all platform-specific code into a unified src/platform/ module directory (9 modules: common, process, fs, shell, env, macos, linux, windows, platform facade)
  • Fixes macOS first-run hang where subprocess calls (xcrun, xcode-select, xlings self init) would block waiting for stdin input — all subprocess calls now auto-redirect stdin from /dev/null on POSIX via platform::process
  • Removes ~550 lines of scattered #ifdef blocks across 7 consumer files
  • Eliminates code duplication: self_exe_path() (was in config.cppm + ninja_backend.cppm), which() (was in config.cppm + probe.cppm), FileLock (was inline in bmi_cache.cppm), shell quoting (was in xlings.cppm)
  • Deletes old src/platform.cppm and src/process.cppm (fully absorbed)

Architecture

src/platform/
├── platform.cppm    # facade — re-exports all sub-modules
├── common.cppm      # compile-time constants + platform detection
├── process.cppm     # unified subprocess execution (stdin fix)
├── fs.cppm          # self_exe_path, which, RAII FileLock
├── shell.cppm       # platform-aware shell quoting
├── env.cppm         # environment variable operations
├── macos.cppm       # xcrun SDK, Xcode CLT detection
├── linux.cppm       # LD_LIBRARY_PATH, runtime lib dirs
└── windows.cppm     # PATH manipulation

Consumers migrated: config, xlings, probe, bmi_cache, ninja_backend, clang, cli.

Test plan

  • Linux CI passes (gcc build + tests)
  • macOS CI passes (llvm build + tests)
  • Windows CI passes (llvm build + tests)
  • First-run on macOS no longer hangs waiting for stdin input

New module mcpp.toolchain.msvc with three discovery strategies:
1. vswhere.exe (Microsoft's official VS locator) — most reliable
2. Environment variables (VSINSTALLDIR, VS*COMNTOOLS)
3. Well-known paths (VS 2017-2025, all editions + BuildTools)

Replaces the hardcoded VS2022 path in clang.cppm with
msvc::find_std_module_source(). Fixes "no std module source"
error on machines with non-standard VS installations.

Also provides find_cl() for future MSVC toolchain support.
Create a unified platform abstraction layer that consolidates all
platform-specific code into dedicated modules under src/platform/:

- common.cppm:   compile-time constants, platform detection, naming
- process.cppm:  unified subprocess execution (auto-closes stdin on
                 POSIX — fixes macOS first-run hang where xcrun/xcode-
                 select would block waiting for user input)
- fs.cppm:       self_exe_path(), which(), RAII FileLock
- shell.cppm:    platform-aware shell argument quoting
- env.cppm:      environment variable operations
- macos.cppm:    Xcode CLT detection, xcrun SDK discovery
- linux.cppm:    LD_LIBRARY_PATH construction, runtime lib dirs
- windows.cppm:  PATH manipulation
- platform.cppm: facade that re-exports all sub-modules

Migrated consumers:
- config.cppm:         use platform::fs::self_exe_path(), which()
- xlings.cppm:         use platform::shell, process, env (all raw
                       popen/system calls removed)
- probe.cppm:          use platform::fs::which(), macos::sdk_path(),
                       linux_::build_ld_library_path_prefix()
- bmi_cache.cppm:      use platform::fs::FileLock (RAII)
- ninja_backend.cppm:  use platform::fs::self_exe_path()
- clang.cppm:          use platform::exe_suffix for tool paths
- cli.cppm:            use platform::name constant

Deleted:
- src/platform.cppm:  replaced by src/platform/common.cppm
- src/process.cppm:   absorbed by src/platform/process.cppm + shell.cppm

Net result: ~550 lines of scattered #ifdef blocks removed, all
subprocess calls now go through platform::process which auto-redirects
stdin from /dev/null on POSIX.
The previous commit removed <cstdio> during cleanup but stderr is still
used in ensure_init(), ensure_patchelf(), and ensure_ninja() warning
messages.
P0: Remove all #define popen/_popen macros from 6 files — migrate
    every raw popen/pclose/std::system call to platform::process APIs.
    Files: cli, ninja_backend, msvc, stdmod, pack, publisher, p1689.
    Add run_passthrough() and extract_exit_code() to platform::process.

P1: Replace all manual shell quoting #ifdefs in cli.cppm with
    platform::shell::quote(). Covers git clone, ninja -C, cmd_run,
    cmd_test (5 locations).

P2: Create platform/terminal.cppm — absorbs is_tty() and
    terminal_cols() from ui.cppm. Eliminates #ifdef __unix__ blocks.

P3: Move kXpkgPlatform constant to platform::common::xpkg_platform.
    resolver.cppm now uses it without #if defined blocks.

P4: Add link strategy constants (supports_full_static, supports_rpath,
    needs_explicit_libcxx) to platform::common. flags.cppm now uses
    if constexpr instead of #if defined blocks.

P5: Replace #if defined(_WIN32) in xlings.cppm build_command_prefix,
    install_with_progress, ensure_init with if constexpr.

P6: Eliminate all #if defined blocks in ninja_backend.cppm rule
    generation. Introduce constexpr $toolenv prefix variable, use
    if constexpr for platform-specific rules (cp_bmi, scan_deps).

Net: ~440 lines of #ifdef removed, 315 lines of clean constexpr/
platform API code added. Zero raw popen calls remain outside
src/platform/.
@Sunrisepeak Sunrisepeak merged commit 0b82803 into main May 20, 2026
3 checks passed
Sunrisepeak added a commit that referenced this pull request May 23, 2026
* fix: seal child-process stdin on Windows (first-run hang)

mcpp's first-run flow on Windows was hanging at xlings / xim / curl / git
grandchildren that block on terminal stdin, forcing users to press Enter
repeatedly to advance bootstrap and toolchain install.

Root cause: process::seal_stdin was a no-op on Windows, and
install_with_progress's direct-install path deliberately bypassed it.
The POSIX side has had </dev/null sealing since PR #55 (macOS xcrun hang
fix); Windows never received the equivalent fix. PR #57 only suppressed
stdout/stderr noise (>/dev/null 2>&1) and did not touch stdin.

Changes:
  - process.cppm: seal_stdin now appends "<NUL" on Windows (matches POSIX
    behavior). All capture / run_silent / run_streaming / run_passthrough
    callers gain the protection automatically.
  - xlings.cppm: install_with_progress's direct path explicitly appends
    "<NUL" on Windows. POSIX keeps the original behavior conservatively.
  - shell.cppm: silent_redirect docstring corrected — it never touched
    stdin, that's seal_stdin's job. Implementation unchanged.

Regression coverage:
  - tests/unit/test_process_seal_stdin.cpp — deterministic reproduction
    test. Rebinds the test process's own stdin to an open, empty,
    never-closing pipe, then calls run_silent / capture / run_streaming
    with a child that reads stdin (more on Windows, cat on POSIX).
    Without the fix the child would block forever waiting on our pipe;
    with the fix it reads NUL / /dev/null and exits immediately. 5-second
    upper bound (real runs complete in <100ms).
  - ci-windows.yml — adds a step that launches mcpp via
    System.Diagnostics.Process with RedirectStandardInput=$true (parent
    holds the child's stdin open but never writes). Runs mcpp --version,
    mcpp build, mcpp run. Without the fix, any grandchild reading stdin
    blocks → step times out → CI fails. With the fix → all complete.

* ci(windows): convert MCPP_SELF MSYS path → Windows path; rename pwsh $Args

Two issues with the regression step from the previous commit (both showed
up only on the actual Windows runner, not in local validation):

1. MCPP_SELF was set in an earlier bash step via `pwd` (git-bash) so the
   value is MSYS-style (e.g. /d/a/mcpp/...). Bash steps tolerate it but
   pwsh's `&` operator can't exec it ("not recognized as a name of a
   cmdlet, function, script file, or executable program"). Convert via
   cygpath -w before use.

2. `$Args` is a PowerShell automatic variable inside function scope; a
   `param([string]$Args)` does not bind cleanly. Renamed to $McppArgs
   to avoid the collision (also updated call sites).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant